package cn.edu.dgut.css.sai.demo.wechat.miniprogramuaa;

import cn.edu.dgut.css.sai.demo.wechat.miniprogramuaa.utils.KeyStoreKeyFactory;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * <li>申请小程序申请号：https://developers.weixin.qq.com/miniprogram/dev/devtools/sandbox.html</li>
 */
@SpringBootApplication
@EnableConfigurationProperties(WeChatProperties.class)
public class MiniProgramUaaApplication {

    public static void main(String[] args) {
        SpringApplication.run(MiniProgramUaaApplication.class, args);
    }

    /**
     * 自定义一个 MappingJackson2HttpMessageConverter，用于转换 微信开放平台的微信网页授权API 的响应内容。
     * <li>默认配置的MappingJackson2HttpMessageConverter不支持text/plain的content-type响应的转换。</li>
     * <li>默认定义的{@link MappingJackson2HttpMessageConverter#getDefaultCharset()}的默认字符集为null，这会导致生成响应请求的头部不会添加chartset=utf-8</li>
     * <li>部分浏览器如safari收到application/json的content-type响应时，如果json字符串包含中文字符会乱码显示。</li>
     * <li>这里声明一个HttpMessageConverter，实例是MappingJackson2HttpMessageConverter，并且配置它的默认字符集为StandardCharsets.UTF_8，当生成响应时，会在原响应content-type后加chartset=UTF-8</li>
     * <li>这个方法的返回值必须是HttpMessageConverter，否则不能注入并配置mvc的{@link HttpMessageConverter},可能是{@link ObjectProvider}的原因。</li>
     * <li>chrome浏览器不受影响。</li>
     */
    @Bean
    HttpMessageConverter<Object> customHttpMessageConverter() {
        MappingJackson2HttpMessageConverter httpMessageConverter = new MappingJackson2HttpMessageConverter();
        List<MediaType> mediaTypes = new ArrayList<>(Collections.singletonList(MediaType.TEXT_PLAIN));
        httpMessageConverter.setSupportedMediaTypes(mediaTypes);
        httpMessageConverter.setDefaultCharset(StandardCharsets.UTF_8);
        return httpMessageConverter;
    }

    @RestController
    @Slf4j
    static class AccessTokenEndpoint {

        final RestTemplate restTemplate;
        final WeChatProperties weChatProperties;
        final KeyPair keyPair;

        AccessTokenEndpoint(RestTemplateBuilder restTemplateBuilder, WeChatProperties weChatProperties, KeyPair keyPair) {
            this.restTemplate = restTemplateBuilder.build();
            this.weChatProperties = weChatProperties;
            this.keyPair = keyPair;
        }

        /**
         * <li>调用 auth.code2Session 接口，换取 用户唯一标识 OpenID 和 会话密钥 session_key ：<br/>
         * https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html</li>
         * <li>unionid机制：https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/union-id.html</li>
         */
        @PostMapping("/accesstoken")
        Object createAccessToken(String code) {
            return Map.of("accesstoken", createJwt(getSession(weChatProperties.getMiniProgram().getAppId(), weChatProperties.getMiniProgram().getAppSecret(), code)));
        }

        public String createJwt(SessionDto sessionDto) {
            Map<String, Object> claims = new HashMap<>();
            claims.put("openid", sessionDto.getOpenid());
            claims.put("session_key", sessionDto.getSession_key());
            // 设置权限标记。Spring Security ResourceServer默认会检查Jwt中claims的scope，如果有这个scope属性会添加一个 SCOPE_前缀+scope属性值 的权限。
            // scope属性值可以设置为列表或数组，这样可以设置多个 SCOPE_xxx 的权限。
            // 在这里可以添加保存用户到数据库，或从数据库读取用户的权限。
            claims.put("scope", Arrays.asList("wechat", "user"));
            // claims.put("scope", "wechat");
            // Spring Security ResourceServer默认会把 Jwt 字符串中 sub字段值 设置 为 Authentication 对象的 name 值，这个值是标识登录用户的，所以这里赋值为小程序授权用户的openid。
            String subject = sessionDto.getOpenid();
            // @formatter:off
            return Jwts.builder()
                    .setClaims(claims)
                    .setSubject(subject)
                    .setIssuedAt(new Date(System.currentTimeMillis()))
                    .setExpiration(new Date(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1)))// 有效期 1 天。
                    .signWith(keyPair.getPrivate(), SignatureAlgorithm.RS256)
                    .compact();
            // @formatter:on
        }

        private SessionDto getSession(Object... uriVariables) {
            ResponseEntity<SessionDto> sessionResponseEntity = this.restTemplate.getForEntity(weChatProperties.getMiniProgram().getCode2SessionApi(), SessionDto.class, uriVariables);
            return Optional.ofNullable(sessionResponseEntity.getBody()).orElseThrow(() -> new RuntimeException("获取openid失败"));
        }

    }

    /**
     * 调用 auth.code2Session 接口返回的数据实体
     */
    @Data
    static class SessionDto {
        private String openid;
        private String session_key;
        private String unionid;
        private Integer errcode;
        private String errmsg;
    }

    /**
     * 在resource目录下生成jwt使用的jks密钥库文件:
     * <p>
     * keytool -genkeypair -alias wechatjks -keyalg RSA -keypass lizhx@dgut.edu.cn -keystore wechat.jks -storepass lizhx@dgut.edu.cn -validity 36500
     * </p>
     * <p>后面的询问，最后一步输入 y ,其它回车即可。</p>
     * <p>
     * 上面的命令生成 2,048 位 RSA 密钥对和自签名证书 (SHA256withRSA) (有效期为 36,500 天)
     * </p>
     */
    @Configuration
    static class KeyConfig {

        /**
         * 加载 JKS密钥库文件
         *
         * @return {@link KeyPair}
         */
        @Bean
        KeyPair keyPair() {
            return new KeyStoreKeyFactory(new ClassPathResource("wechat.jks"), "lizhx@dgut.edu.cn".toCharArray())// 密钥对的文件、密码
                    .getKeyPair("wechatjks");// 生成密钥对时设置的别名-alias
        }

    }

}

